﻿// Deep Earth is a community project available under the Microsoft Public License (Ms-PL)
// Code is provided as is and with no warranty  Use at your own risk
// View the project and the latest code at http://DeepEarth.codeplex.com/

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using DeepEarth.Client.MapControl.Controls;
using DeepEarth.Client.MapControl.Events;
using DeepEarth.Client.MapControl.Geometries;
using DeepEarth.Client.MapControl.Layers;
using DeepEarth.Client.MapControl.Utilities;

namespace DeepEarth.Client.MapControl
{
    /// <summary>
    /// <para>
    /// Map is the root of the DeepEarth.Client.MapControl object model.  It provides a base structure and accessors for:
    /// <list type="bullet">
    /// <item>Controls</item>
    /// <item>Events</item>
    /// <item>Geometry</item>
    /// <item>Layers</item>
    /// </list>
    /// </para>
    /// 
    /// <example>
    /// <code lang="CSharp" title="Create an instance of map and set a TileSource">   
    ///    Map map = new Map();
    ///    map.BaseLayer.Source = new VeTileSource(VeMapModes.VeHybrid);
    /// </code>
    /// </example>
    /// </summary>
    [TemplatePart(Name = PART_LicenseContainer, Type = typeof(Panel))]
    [TemplatePart(Name = PART_MapContent, Type = typeof(Grid))]
    [TemplatePart(Name = PART_MapRotation, Type = typeof(RotateTransform))]
    [TemplatePart(Name = PART_MouseControl, Type = typeof(MouseControl))]
    [TemplatePart(Name = PART_MapLayers, Type = typeof(Grid))]
    [TemplatePart(Name = PART_ModeCrossFade, Type = typeof(Image))]
    [ContentProperty("Layers")]
    public class Map : Control
    {
        private const string PART_LicenseContainer = "PART_LicenseContainer";
        private const string PART_MapContent = "PART_MapContent";
        private const string PART_MapLayers = "PART_MapLayers";
        private const string PART_MapRotation = "PART_MapRotation";
        private const string PART_MouseControl = "PART_MouseControl";
        private const string PART_ModeCrossFade = "PART_ModeCrossFade";

        /// <summary>
        /// Provides a reference to an instance the last created Map.  This is be applicable for cases where only one Map instance is created.
        /// Or where map specific values are not relevant.
        /// </summary>
        public static Map DefaultInstance;

        private readonly TileLayer _baseLayer;
        private readonly ObservableCollection<ILayer> _layers;

        private DoubleAnimation _animationMapAngle;
        private CoordTransform _coordHelper;
        private DragBehavior _dragMode;
        private Panel _licenseContainer;
        private Grid _mapContent;
        private Grid _mapLayerGrid;
        private RotateTransform _mapRotation;
        private ISpatialReference _spatialReference;
        private Storyboard _storyboardRotation;
        private Point _targetCenter;
        private Image _modeCrossFadeImage;

        internal double _TargetZoom;
        internal double _RotationAngle;

        /// <summary>
        /// Create an instance of the DeepEarth.Client.MapControl Map Control
        /// </summary>
        public Map()
        {
            DefaultStyleKey = typeof(Map);

            DefaultInstance = this;

            Events = new MapEvents(this);
            BoundingBox = new MouseControl(this);

            _baseLayer = new TileLayer(this);
            _baseLayer.Msi.UseSprings = true;
            _baseLayer.Msi.IsHitTestVisible = true;
            _baseLayer.Msi.BlurFactor = 0.5D;
            _baseLayer.Msi.GetImageOpenSucceeded().Subscribe(@event => AnimateBlur.Blur(this));

            _layers = new ObservableCollection<ILayer>();
            _spatialReference = new MercatorProjection();

            _layers.CollectionChanged += Layers_CollectionChanged;

            //Create event handlers for default map behaviour
            this.GetMapLoaded().Subscribe(@event => UpdateStartupPoint());
            this.GetMapTileSourceChanged().Subscribe(@event => UpdateCopyrights());
            // this.GetDoubleClick().Subscribe(MapDoubleClick);
            this.GetMapZoomEnded().Subscribe(@event =>
                {
                    _TargetZoom = 0;
                    AnimateBlur.Blur(this);
                });

            Events.MapDoubleClick += Events_MapDoubleClick;

            //Wire up the keyboard events.
            KeyDown += (o, e) => Events.KeyDown(this, e);
            KeyUp += (o, e) => Events.KeyUp(this, e);
            TabNavigation = KeyboardNavigationMode.Once;
            IsTabStop = true;
        }

        /// <summary>
        /// Overrides the default OnApplyTemplate to configure child FrameworkElement references from the applied template
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            //Get instances from the template
            _mapContent = (Grid)GetTemplateChild(PART_MapContent);
            _mapLayerGrid = (Grid)GetTemplateChild(PART_MapLayers);
            _mapRotation = (RotateTransform)GetTemplateChild(PART_MapRotation);
            _licenseContainer = (Panel)GetTemplateChild(PART_LicenseContainer);
            _modeCrossFadeImage = (Image)GetTemplateChild(PART_ModeCrossFade);

            BoundingBox = (MouseControl)GetTemplateChild(PART_MouseControl);
            BoundingBox.MapInstance = this;

            _mapContent.Children.Insert(0, BaseLayer);

            //Perform any necessary initialization to the elements
            RotationAngle = _mapRotation.Angle;

            ConfigMapLayers();
            ConfigMapRotation();

            LayoutUpdated += (o, e) => UpdateClippingArea();
            //BaseLayer.MSI.ViewportChanged += (o, e) => UpdateClippingArea();
        }

        public void CrossFadeTransistion()
        {
            if (_mapContent != null)
            {
                //capture current screen in writeable bitmap
                var bitmap = new WriteableBitmap(this._mapContent, new TranslateTransform());
                _modeCrossFadeImage.Source = bitmap;

                //kick off animation
                var sb = new Storyboard();
                var daf1 = new DoubleAnimationUsingKeyFrames { Duration = new Duration(new TimeSpan(0, 0, 0, 1, 500)) };
                daf1.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
                daf1.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1.0)), Value = 1 });
                daf1.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1.5)), Value = 0 });
                Storyboard.SetTarget(daf1, _modeCrossFadeImage);
                Storyboard.SetTargetProperty(daf1, new PropertyPath("UIElement.Opacity"));
                sb.Children.Add(daf1);
                sb.Begin();
            }
        }

        /// <summary>
        /// Layers is an ObservableCollection of ILayer instances.
        /// Each layer is placed in a grid above the Map.BaseLayer
        /// </summary>
        public ObservableCollection<ILayer> Layers
        {
            get { return _layers; }
        }

        /// <summary>
        /// The Behaviour of the control while dragging the mouse over the map control.
        /// </summary>
        public DragBehavior DragMode
        {
            get { return _dragMode; }
            set
            {
                if (value == DragBehavior.Draw) Cursor = Cursors.Stylus;
                if (value == DragBehavior.Pan) Cursor = Cursors.Hand;
                if (value == DragBehavior.Select) Cursor = Cursors.Arrow;
                _dragMode = value;
                Events.DragBehavourChanged(this, new MapEventArgs());
            }
        }

        /// <summary>
        /// Reference to an instance of the CoordTranform class.  
        /// </summary>
        public CoordTransform CoordHelper
        {
            get { return _coordHelper ?? (_coordHelper = new CoordTransform(this)); }
        }

        /// <summary>
        /// The current Spatial Reference of the Map Control
        /// </summary>
        public ISpatialReference SpatialReference
        {
            get { return _spatialReference ?? (_spatialReference = new SpatialReference()); }
            set { _spatialReference = value; }
        }

        /// <summary>
        /// Access to all the events of the map including 
        /// <list type="bullet">
        /// <item>Mouse</item>
        /// <item>Keyboard</item>
        /// <item>Map Specific</item>
        /// </list>
        /// </summary>
        public MapEvents Events { get; private set; }

        /// <summary>
        /// Provides public access to the MouseControl.
        /// </summary>
        public MouseControl BoundingBox { get; set; }

        /// <summary>
        /// The main raster TileLayer of the map. This is the DeepZoom tile set that is the core of DeepEarth.Client.MapControl
        /// </summary>
        public TileLayer BaseLayer
        {
            get { return _baseLayer; }
        }

        /// <summary>
        /// The unit of measure we display information in.
        /// </summary>
        [DefaultValue(Unit.Meter)]
        public Unit DisplayUnit { get; set; }

        /// <summary>
        /// The current scale of the map at the centre of the current screen.
        /// EG 1:1000 meters, one meter of the screen would equal 1000 real meters
        /// </summary>
        public double Scale
        {
            get
            {
                return BaseLayer.Source != null ? CoordHelper.GetScaleAtZoomLevel(ZoomLevel, GeoCenter, DisplayUnit) : 0;
            }
        }

        /// <summary>
        /// The resolution of a pixel at the current ZoomLevel at the equator.
        /// </summary>
        public double Resolution
        {
            get
            {
                return BaseLayer.Source != null ? CoordHelper.GetResolutionAtZoomLevel(ZoomLevel, DisplayUnit) : 0;
            }
        }

        /// <summary>
        /// The current Zoom level of the map, an exponential scale where level 3 is twice the width and height of level 4
        /// Typically level 1 will be the whole world at 512px, 17 is street level.
        /// Larger value = zoom into the world. 
        /// </summary>
        public double ZoomLevel
        {
            get
            {
                if (BaseLayer.IsLoaded && BaseLayer.Source != null)
                {
                    var zoom = CoordHelper.LogicalViewToZoomLevel(MapViewLogicalSize);
                    return zoom;
                }
                return -1;
            }
            set
            {
                //Validate against zoom level being set out of bounds.
                var validatedZoom = BaseLayer.Source.GetValidatedZoomLevel(value);
                if (BaseLayer.IsLoaded)
                {
                    var center = new Point(BaseLayer.Msi.ActualWidth / 2, BaseLayer.Msi.ActualHeight / 2);
                    ZoomOnPixelPoint(center, validatedZoom - ZoomLevel);
                }
                else
                {
                    //Cache value to be applied post MSI.Loaded
                    _TargetZoom = validatedZoom;
                }
            }
        }

        /// <summary>
        /// The Geographic Centre point of the Map (Longitude, Latitude)
        /// </summary>
        public Location GeoCenter
        {
            get
            {
                if (SpatialReference != null)
                {
                    var centerPixel = new Point(BaseLayer.Msi.ActualWidth / 2, BaseLayer.Msi.ActualHeight / 2);
                    return CoordHelper.PixelToGeo(centerPixel);
                }

                return new Location();
            }
            set
            {
                if (SpatialReference != null && BaseLayer.Msi != null)
                {
                    var pixelCenter = CoordHelper.GeoToPixel(value);
                    var pixelOrigin = new Point(pixelCenter.X - BaseLayer.Msi.ActualWidth / 2,
                                                pixelCenter.Y - BaseLayer.Msi.ActualHeight / 2);
                    GeoOrigin = CoordHelper.PixelToGeo(pixelOrigin);
                }
                else
                {
                    //Cache value to be applied post MSI.Loaded
                    _targetCenter = new Point(value.Longitude, value.Latitude);
                }
            }
        }

        /// <summary>
        /// The Geographical top left of the Map (Longitude, Latitude)
        /// </summary>
        public Location GeoOrigin
        {
            get
            {
                return (BaseLayer.Msi != null) ? CoordHelper.LogicalToGeo(BaseLayer.Msi.ViewportOrigin) : new Location();
            }
            set
            {
                if (BaseLayer.Msi != null)
                {
                    BaseLayer.Msi.ViewportOrigin = CoordHelper.GeoToLogical(value);
                }
            }
        }

        /// <summary>
        /// The current Geographical Bounds (extent) of the map (Longitude, Latitude)
        /// </summary>
        public Rect GeoBounds
        {
            get
            {
                Rect bounds;
                if (BaseLayer.Msi != null)
                {
                    var geoOrigin = GeoOrigin;
                    var pixelExtent = new Point(PixelOrigin.X + BaseLayer.Msi.ActualWidth, PixelOrigin.Y + BaseLayer.Msi.ActualHeight);
                    var geoExtent = CoordHelper.PixelToGeo(pixelExtent);

                    bounds = new Rect(new Point(geoOrigin.Longitude, geoOrigin.Latitude), new Point(geoExtent.Longitude, geoExtent.Latitude));
                }
                else
                {
                    bounds = new Rect();
                }
                return bounds;
            }
            set
            {
                if (BaseLayer.Msi != null && BaseLayer.Source != null)
                {
                    // Point pixelBoxOrigin = CoordHelper.GeoToPixel(new Point(value.Left, value.Top));
                    // Point pixelBoxExtent = CoordHelper.GeoToPixel(new Point(value.Right, value.Bottom));
                    var pixelBoxOrigin = CoordHelper.GeoToPixel(new Location { Longitude = value.Left, Latitude = value.Top });
                    var pixelBoxExtent = CoordHelper.GeoToPixel(new Location { Longitude = value.Right, Latitude = value.Bottom });
                    var pixelBox = new Rect(pixelBoxOrigin, pixelBoxExtent);

                    SetPixelBounds(pixelBox);
                }
            }
        }

        /// <summary>
        /// The relative Pixel of the centre of the map. (X,Y)
        /// Always the half the width, half the right
        /// Useful if you want to pan X pixels.
        /// </summary>
        public Point PixelCenter
        {
            get
            {
                return (BaseLayer.Msi != null) ? new Point(BaseLayer.Msi.ActualWidth / 2, BaseLayer.Msi.ActualHeight / 2) : new Point();
            }
            set
            {
                if (SpatialReference != null && BaseLayer.Msi != null)
                {
                    var pixelCenter = CoordHelper.GeoToPixel(new Location { Longitude = value.X, Latitude = value.Y });
                    var pixelOrigin = new Point(pixelCenter.X - BaseLayer.Msi.ActualWidth / 2, pixelCenter.Y - BaseLayer.Msi.ActualHeight / 2);

                    GeoOrigin = CoordHelper.PixelToGeo(pixelOrigin);
                }
                else
                {
                    //Cache value to be applied post MSI.Loaded
                    _targetCenter = value;
                }
            }
        }

        /// <summary>
        /// The absolute Pixel of the top left of the map. (X,Y)
        /// </summary>
        public Point PixelOrigin
        {
            get
            {
                return (BaseLayer.Msi != null) ? CoordHelper.LogicalToPixel(LogicalOrigin) : new Point();
            }
            set
            {
                if (BaseLayer.Msi != null)
                {
                    BaseLayer.Msi.ViewportOrigin = CoordHelper.PixelToLogical(value);
                }
            }
        }

        /// <summary>
        /// The absolute Pixel bounds of the map. (X,Y)
        /// </summary>
        public Rect PixelBounds
        {
            get
            {
                Rect bounds;
                if (BaseLayer.Msi != null)
                {
                    var pixelOrgin = PixelOrigin;
                    var pixelExtent = new Point(PixelOrigin.X + BaseLayer.Msi.ActualWidth, PixelOrigin.Y + BaseLayer.Msi.ActualHeight);

                    bounds = new Rect(pixelOrgin, pixelExtent);
                }
                else
                {
                    bounds = new Rect();
                }
                return bounds;
            }
            set
            {
                if (BaseLayer.Msi != null && BaseLayer.Source != null)
                {
                    SetPixelBounds(value);
                }
            }
        }

        /// <summary>
        /// The Logical (0->1) centre of the Map.
        /// </summary>
        public Point LogicalCenter
        {
            get
            {
                if (BaseLayer.Msi != null)
                {
                    var aspectRatio = BaseLayer.Msi.ActualHeight / BaseLayer.Msi.ActualWidth;
                    var viewPortHight = BaseLayer.Msi.ViewportWidth * aspectRatio;
                    var logCenter = new Point(BaseLayer.Msi.ViewportOrigin.X + BaseLayer.Msi.ViewportWidth / 2, BaseLayer.Msi.ViewportOrigin.Y + viewPortHight / 2);

                    return logCenter;
                }

                return new Point();
            }
            set
            {
                if (SpatialReference != null && BaseLayer.Msi != null)
                {
                    var pixelCenter = CoordHelper.GeoToPixel(new Location { Longitude = value.X, Latitude = value.Y });
                    var pixelOrigin = new Point(pixelCenter.X - BaseLayer.Msi.ActualWidth / 2, pixelCenter.Y - BaseLayer.Msi.ActualHeight / 2);

                    GeoOrigin = CoordHelper.PixelToGeo(pixelOrigin);
                }
                else
                {
                    //Cache value to be applied post MSI.Loaded
                    _targetCenter = value;
                }
            }
        }

        /// <summary>
        /// The Logical (0->1) top left of the Map.
        /// </summary>
        public Point LogicalOrigin
        {
            get
            {
                return (BaseLayer.Msi != null) ? BaseLayer.Msi.ViewportOrigin : new Point();
            }
            set
            {
                if (BaseLayer.Msi != null)
                {
                    BaseLayer.Msi.ViewportOrigin = value;
                }
            }
        }

        /// <summary>
        /// The Logical (0->1) bounds of the Map.
        /// </summary>
        public Rect LogicalBounds
        {
            get
            {
                Rect bounds;
                if (BaseLayer.Msi != null)
                {
                    var pixelExtent = new Point(PixelOrigin.X + BaseLayer.Msi.ActualWidth, PixelOrigin.Y + BaseLayer.Msi.ActualHeight);
                    var logicalExtent = CoordHelper.PixelToLogical(pixelExtent);

                    bounds = new Rect(LogicalOrigin, logicalExtent);
                }
                else
                {
                    bounds = new Rect();
                }
                return bounds;
            }
            set
            {
                if (BaseLayer.Msi != null && BaseLayer.Source != null)
                {
                    var pixelBoxOrigin = CoordHelper.LogicalToPixel(new Point(value.Left, value.Top));
                    var pixelBoxExtent = CoordHelper.LogicalToPixel(new Point(value.Right, value.Bottom));
                    var pixelBox = new Rect(pixelBoxOrigin, pixelBoxExtent);

                    SetPixelBounds(pixelBox);
                }
            }
        }

        /// <summary>
        /// The current view of the map's logical (0->1) size.
        /// </summary>
        public Size MapViewLogicalSize
        {
            get { return new Size(BaseLayer.Msi.ViewportWidth, BaseLayer.Msi.ViewportWidth * AspectRatio); }
        }

        /// <summary>
        /// The current size of the map in Pixels, e.g. 800,600.
        /// </summary>
        public Size MapViewPixelSize
        {
            get
            {
                return (BaseLayer.Msi != null) ? new Size(BaseLayer.Msi.ActualWidth, BaseLayer.Msi.ActualHeight) : new Size(ActualWidth, ActualHeight);
            }
        }

        /// <summary>
        /// The current size of the map regardless of the ViewPort, 
        /// (e.g. If viewing only half the map (e.g. eastern hemisphere), a ViewPort of 400,400 pixels, the MapExtent is 800, 800  /// </summary>
        public Size MapExtentPixelSize
        {
            get
            {
                Size extent;

                if (BaseLayer != null && BaseLayer.Source != null)
                {
                    var mapWidth = MapViewPixelSize.Width / MapViewLogicalSize.Width;
                    var mapHeight = mapWidth * (BaseLayer.Source.TileSize.Height / BaseLayer.Source.TileSize.Width);

                    extent = new Size(mapWidth, mapHeight);
                }
                else
                {
                    extent = (ActualHeight > 0 && ActualHeight > 0) ? new Size(ActualWidth, ActualHeight) : new Size(0, 0);
                }
                return extent;
            }
        }

        /// <summary>
        /// The Ratio of Width to Height, of the screen ViewPort
        /// Important to be able to calculate the actual height only given the width.
        /// </summary>
        public double AspectRatio
        {
            get { return (BaseLayer.Msi.ActualHeight / BaseLayer.Msi.ActualWidth); }
        }

        /// <summary>
        /// Has the Map been loaded yet? If it hasn't then many properties and methods will not yet be useable.
        /// Consider subscribing to the Map.Events.MapLoaded event
        /// </summary>
        public bool IsMapLoaded
        {
            get { return BaseLayer.IsLoaded; }
        }

        /// <summary>
        /// The current rotation angle of the map in degrees. For example, setting of 180 would make south at the top.
        /// </summary>
        [DefaultValue(0)]
        public double RotationAngle
        {
            get { return _RotationAngle; }
            set
            {
                _RotationAngle = value;
                if (_animationMapAngle != null)
                {
                    _animationMapAngle.To = RotationAngle;
                    _storyboardRotation.Begin();
                    Events.RotationChanged(this, new MapEventArgs());
                }
                else
                {
                    _mapRotation.Angle = _RotationAngle;
                }

            }
        }

        #region Map Public Methods

        /// <summary>
        /// Zoom the map by an amount of ZoomLevel on a specific point (in relative pixels from top left)
        /// This point is zooms in on a pivot point, not necessarily centre screen
        /// Useful for interactive zooming such as zoom in/out holding the geo location under the mouse stable. 
        /// </summary>
        /// <param name="pixelPoint">Point to zoom on, relative pixels from top left</param>
        /// <param name="adjustment">The amount of ZoomLevel to zoom in (+) or zoom out (-)</param>
        public void ZoomOnPixelPoint(Point pixelPoint, double adjustment)
        {
            if (BaseLayer.Source != null && BaseLayer.Msi != null && BaseLayer.Source.IsInitialized && Events.EnableMapZoom)
            {
                //Do not update the TargetZoom until the requested zoom level has been validated to be in range.
                var requestedZoom = (_TargetZoom == 0) ? Math.Round(ZoomLevel) + adjustment : _TargetZoom + adjustment;
                if (BaseLayer.Source.IsValidZoomLevel(requestedZoom))
                {
                    _TargetZoom = requestedZoom;
                    var factorX = pixelPoint.X / MapViewPixelSize.Width;
                    var factorY = pixelPoint.Y / MapViewPixelSize.Height;
                    var logicalPoint = CoordHelper.PixelToLogical(pixelPoint);
                    var viewSize = CoordHelper.ZoomLevelToLogicalView(_TargetZoom);
                    var targetLogicalOrigin = new Point
                                              {
                                                  X = logicalPoint.X - (viewSize.Width * factorX),
                                                  Y = logicalPoint.Y - (viewSize.Height * factorY)
                                              };

                    BaseLayer.Msi.ViewportOrigin = targetLogicalOrigin;
                    BaseLayer.Msi.ViewportWidth = viewSize.Width;
                }
            }
        }

        /// <summary>
        /// Pan the map (move it) the default amount (10%) in a standard direction 
        /// </summary>
        /// <param name="direction">The direction to pan, traditional 8 point of the compass</param>
        public void Pan(Direction direction)
        {
            var mapSize = MapViewPixelSize;
            var offset = mapSize.Width / 10;

            switch (direction)
            {
                case Direction.North:
                    Pan(0, -offset);
                    break;

                case Direction.NorthEast:
                    Pan(offset, -offset);
                    break;

                case Direction.NorthWest:
                    Pan(-offset, -offset);
                    break;

                case Direction.South:
                    Pan(0, offset);
                    break;

                case Direction.SouthEast:
                    Pan(offset, offset);
                    break;

                case Direction.SouthWest:
                    Pan(-offset, offset);
                    break;

                case Direction.East:
                    Pan(offset, 0);
                    break;

                case Direction.West:
                    Pan(-offset, 0);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("direction");
            }
        }

        /// <summary>
        /// Pan the Map (Move it) by the supplied pixels in both X and or Y planes
        /// </summary>
        /// <param name="deltaXInPixels">The number of pixels to pan left (-) or right (+)</param>
        /// <param name="deltaYInPixels">The number of pixels to pan up (-) or down (+)</param>
        public void Pan(double deltaXInPixels, double deltaYInPixels)
        {
            var offset = PixelOrigin;

            //consider rotation
            var deltaRotated = CoordHelper.RotatePixelbyMapRotation(new Point(deltaXInPixels, deltaYInPixels));
            offset.X += deltaRotated.X;
            offset.Y += deltaRotated.Y;

            PixelOrigin = offset;
        }

        /// <summary>
        /// Allows client applications to forces a refresh of tiles.  This is useful when a client application has dependent layers which need to 
        /// be  required.  For example, you might have a data driven Geometry Layer in which a parameter change requires a re-fetch.  Since the layer is
        /// driven by the ITileSource GetTile calls, a RefreshSource would allow a re-fetch to be made based on the current ViewPort.
        /// </summary>
        public void RefreshSource()
        {
            MultiScaleTileSource source = BaseLayer.Msi.Source;
            BaseLayer.Msi.Source = source;
        }

        /// <summary>
        /// Centre the map on the specified Long,Lat at the specified ZoomLevel
        /// </summary>
        /// <param name="geoCenter">Geographical Longitude, Latitude Point</param>
        /// <param name="zoom">The Map ZoomLevel required</param>
        public void SetViewCenter(Location geoCenter, double zoom)
        {
            if (BaseLayer.Msi != null && BaseLayer.Msi.ActualWidth != 0)
            {
                var viewSize = CoordHelper.ZoomLevelToLogicalView(zoom);
                var logicalCenter = CoordHelper.GeoToLogical(geoCenter);
                var logicalOrigin = new Point
                                    {
                                        X = logicalCenter.X - (viewSize.Width / 2),
                                        Y = logicalCenter.Y - (viewSize.Height / 2)
                                    };

                BaseLayer.Msi.ViewportOrigin = logicalOrigin;
                BaseLayer.Msi.ViewportWidth = viewSize.Width;
            }
            else
            {
                _targetCenter = new Point(geoCenter.Longitude, geoCenter.Latitude);
                _TargetZoom = zoom;
            }
        }

        /// <summary>
        /// Set the view of the map to be centred on 0,0 and fit the Map extent width to the screen
        /// </summary>
        public void SetViewFullMap()
        {
            var zoom = CoordHelper.LogicalViewToZoomLevel(new Size(1, 1));
            SetViewCenter(new Location { Longitude = 0, Latitude = 0 }, zoom);
        }

        private bool _isStartupPointSet;
        internal void UpdateStartupPoint()
        {
            //Make sure this only executed once.  
            if (_isStartupPointSet == false)
            {
                _isStartupPointSet = true;

                //If centre is not set, let centre on Map centre.
                var isCenterSet = _targetCenter != new Point();
                var isZoomSet = _TargetZoom != 0;

                if (isCenterSet || isZoomSet)
                {
                    //If centre value was not set, set centre to the logical centre point.
                    if (isCenterSet == false)
                    {
                        var logical = CoordHelper.LogicalToGeo(new Point(0.5, 0.5));
                        _targetCenter = new Point(logical.Longitude, logical.Latitude);
                    }

                    SetViewCenter(new Location { Longitude = _targetCenter.X, Latitude = _targetCenter.Y }, _TargetZoom);
                }
                else
                {
                    SetViewFullMap();
                }
            }
        }

        #endregion

        private void SetPixelBounds(Rect pixelBox)
        {
            //Check to make sure this wasn't a simple click by checking the distance moved by mouse
            var area = pixelBox.Width * pixelBox.Height;

            if (area > 100)
            {
                var pixelCenter = new Point
                                  {
                                      X = pixelBox.X + pixelBox.Width / 2,
                                      Y = pixelBox.Y + pixelBox.Height / 2
                                  };

                //Get the minimum of the bounding box proportions to the actual screen.
                var percWidth = pixelBox.Width / MapViewPixelSize.Width;
                var percHeight = pixelBox.Height / MapViewPixelSize.Height;
                var maxPerc = Math.Max(percWidth, percHeight);
                var targetWidth = BaseLayer.Msi.ViewportWidth * maxPerc;
                var targetZoom = CoordHelper.LogicalViewToZoomLevel(new Size(targetWidth, targetWidth));

                //Try to round the zoom level and redo the size off the new value;
                var roundZoom = (int)Math.Round(targetZoom, 0);
                var roundedView = CoordHelper.ZoomLevelToLogicalView(roundZoom);
                var logicalOrigin = new Point();
                var logicalCenter = CoordHelper.PixelToLogical(pixelCenter);

                logicalOrigin.X = logicalCenter.X - (roundedView.Width / 2);
                logicalOrigin.Y = logicalCenter.Y - (roundedView.Height / 2);

                BaseLayer.Msi.ViewportOrigin = logicalOrigin;
                BaseLayer.Msi.ViewportWidth = roundedView.Width;
            }
        }

        private void ConfigMapLayers()
        {
            foreach (var mapItem in Layers)
            {
                mapItem.MapInstance = this;

                if (_mapLayerGrid.Children.Contains((UIElement)mapItem) == false)
                {
                    _mapLayerGrid.Children.Add((UIElement)mapItem);
                }
            }
        }

        private void ConfigMapRotation()
        {
            _storyboardRotation = new Storyboard();
            _animationMapAngle = new DoubleAnimation { Duration = new Duration(TimeSpan.FromMilliseconds(250)) };
            Storyboard.SetTarget(_animationMapAngle, _mapContent);
            Storyboard.SetTargetProperty(_animationMapAngle, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
            _storyboardRotation.Children.Add(_animationMapAngle);
        }

        private void Events_MapDoubleClick(object sender, MouseButtonEventArgs args)
        {
            if (this.Events.EnableMapZoomOnDoubleClick)
            {
                var isShiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
                var adjustment = isShiftDown ? -1 : 1;

                var pixel = args.GetPosition(this);
                this.ZoomOnPixelPoint(pixel, adjustment);
            }
        }

        //private static void MapDoubleClick(TimeInterval<IEvent<MouseButtonEventArgs>> timeInterval)
        //{
        //    var map = (Map)timeInterval.Value.Sender;

        //    if (map.Events.EnableMapZoomOnDoubleClick)
        //    {
        //        var isShiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
        //        var adjustment = isShiftDown ? -1 : 1;

        //        var pixel = timeInterval.Value.EventArgs.GetPosition(map);
        //        map.ZoomOnPixelPoint(pixel, adjustment);
        //    }
        //}

        private void UpdateCopyrights()
        {
            if (_licenseContainer != null)
            {
                _licenseContainer.Children.Clear();

                // var copyrights = new List<UIElement>();
                if (BaseLayer.Source != null)
                {
                    var copyright = BaseLayer.Source.GetCopyright();
                    if (copyright != null)
                    {
                        _licenseContainer.Children.Add(new ContentControl { Content = copyright });
                    }
                }

                foreach (var overlay in BaseLayer.Overlays)
                {
                    var copyright = overlay.GetCopyright();
                    if (copyright != null)
                    {
                        _licenseContainer.Children.Add(new ContentControl { Content = copyright });
                    }
                }
            }
        }

        /// <summary>
        /// Walks up the visual tree from the element supplied and gets the actual Map Instance
        /// Used by any control in the template that need to have access to the Map
        /// </summary>
        /// <param name="element">The current context element, typically "this"</param>
        /// <returns>The actual instance of the Map control</returns>
        public static Map GetMapInstance(FrameworkElement element)
        {
            Map mapInst;
            var parent = element;
            while (parent != null && parent is Map == false)
            {
                parent = (FrameworkElement)VisualTreeHelper.GetParent(parent);
            }
            if (parent != null)
            {
                mapInst = (Map)parent;
            }
            else
            {
                //Try secondary method to get instance
                mapInst = DefaultInstance;
            }
            return mapInst;
        }

        private void UpdateClippingArea()
        {
            if (ActualHeight > 0 && ActualWidth > 0)
            {
                var clip = new RectangleGeometry { Rect = new Rect(new Point(0, 0), new Size(ActualWidth, ActualHeight)) };
                Clip = clip;
            }
        }

        private void Layers_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (IsMapLoaded)
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        {
                            var index = e.NewStartingIndex;
                            foreach (var obj in e.NewItems)
                            {
                                if (obj is TileLayer)
                                {
                                    var tileSource = (TileLayer)e.NewItems[0];
                                    tileSource.Msi.UseSprings = false;
                                    tileSource.Msi.IsHitTestVisible = false;
                                    tileSource.Msi.BlurFactor = 0.5D;

                                    tileSource.Msi.ViewportOrigin = this.BaseLayer.Msi.ViewportOrigin;
                                    tileSource.Msi.ViewportWidth = this.BaseLayer.Msi.ViewportWidth;

                                    this.BaseLayer.Msi.GetViewportChanged().Subscribe(@event =>
                                    {
                                        tileSource.Msi.ViewportOrigin = this.BaseLayer.Msi.ViewportOrigin;
                                        tileSource.Msi.ViewportWidth = this.BaseLayer.Msi.ViewportWidth;
                                    });

                                    //this.BaseLayer.Msi.GetViewportChanged().Next();


                                    //var bindingViewportOrigin = new Binding();
                                    //bindingViewportOrigin.Path = new PropertyPath("ViewportOrigin");
                                    //bindingViewportOrigin.Mode = BindingMode.OneWay;
                                    //bindingViewportOrigin.Source = BaseLayer.Msi;
                                    //tileSource.Msi.SetBinding(MultiScaleImage.ViewportOriginProperty, bindingViewportOrigin);


                                    //var bindingViewportWidth = new Binding();
                                    //bindingViewportWidth.Path = new PropertyPath("ViewportWidth");
                                    //bindingViewportWidth.Mode = BindingMode.OneWay;
                                    //bindingViewportWidth.Source = BaseLayer.Msi;
                                    //tileSource.Msi.SetBinding(MultiScaleImage.ViewportWidthProperty, bindingViewportWidth);

                                }

                                if (obj is ILayer)
                                {
                                    // Set the layers MapInstance just in case the layer is created outside the Maps Visual Tree.
                                    // This is a performance improvement because layers do not have to walk the visual tree for MapInstance
                                    var layer = (ILayer)e.NewItems[0];
                                    layer.MapInstance = this;
                                    _mapLayerGrid.Children.Insert(index, obj as UIElement);
                                    index++;
                                }
                            }
                            break;
                        }
                    case NotifyCollectionChangedAction.Remove:
                        {
                            foreach (var obj in e.OldItems)
                            {
                                if (obj is UIElement)
                                {
                                    var removalItem = obj as UIElement;
                                    if (_mapLayerGrid.Children.Contains(removalItem))
                                    {
                                        _mapLayerGrid.Children.RemoveAt(e.OldStartingIndex);
                                    }
                                }
                            }
                            break;
                        }
                }
            }
            UpdateCopyrights();
        }

        private void UpdateChildTileLayersView()
        {

        }
    }
}